﻿// Copyright © Microsoft Corporation.  All Rights Reserved.
// This code released under the terms of the 
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)

namespace Expression.Samples.PathListBoxUtils
{
	using System;
	using System.Windows;
	using System.Windows.Controls;
	using System.Windows.Media;
	using System.Windows.Media.Animation;
	using Microsoft.Expression.Controls;

	public class PathListBoxItemTransformer : ContentControl
	{
		#region Private Properties
		private IPathLayoutItem pathLayoutItem;
		private PathListBox pathListBox;
		private TransformGroup transformGroup;

		#endregion

		#region DependencyProperty AdjustedOffset
		/// <summary>
		/// Gets the value of the offset after it has been adjusted by Ease, Shift and IsCentered.
		/// </summary>
		public double AdjustedOffset
		{
			get { return (double)this.GetValue(PathListBoxItemTransformer.AdjustedOffsetProperty); }
			internal set { this.SetValue(PathListBoxItemTransformer.AdjustedOffsetProperty, value); }
		}

		public static readonly DependencyProperty AdjustedOffsetProperty =
			DependencyProperty.Register("AdjustedOffset", typeof(double), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(0.0, new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));

		#endregion

		#region DependencyProperty AngleRange
		/// <summary>
		/// Gets or sets the range of angle values to add to the rotation.
		/// </summary>
		public Point AngleRange
		{
			get { return (Point)this.GetValue(PathListBoxItemTransformer.AngleRangeProperty); }
			set { this.SetValue(PathListBoxItemTransformer.AngleRangeProperty, value); }
		}

		public static readonly DependencyProperty AngleRangeProperty =
			DependencyProperty.Register("AngleRange", typeof(Point), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(new Point(0, 0), new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion

		#region DependencyProperty Ease
		/// <summary>
		/// Gets or sets the easing function that is used to adjust the offset.
		/// </summary>
		public EasingFunctionBase Ease
		{
			get { return (EasingFunctionBase)this.GetValue(PathListBoxItemTransformer.EaseProperty); }
			set { this.SetValue(PathListBoxItemTransformer.EaseProperty, value); }
		}

		public static readonly DependencyProperty EaseProperty =
			DependencyProperty.Register("Ease", typeof(EasingFunctionBase), typeof(PathListBoxItemTransformer),
			new PropertyMetadata(null, new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion

		#region DependencyProperty IsCentered
		/// <summary>
		/// Gets or sets whether to adjust the offset evenly from the center of the path.
		/// </summary>
		public bool IsCentered
		{
			get { return (bool)this.GetValue(PathListBoxItemTransformer.IsCenteredProperty); }
			set { this.SetValue(PathListBoxItemTransformer.IsCenteredProperty, value); }
		}

		public static readonly DependencyProperty IsCenteredProperty =
			DependencyProperty.Register("IsCentered", typeof(bool), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(false, new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion

		#region DependencyProperty OpacityRange
		/// <summary>
		/// Gets or sets the range of values to use for the opacity.
		/// </summary>
		public Point OpacityRange
		{
			get { return (Point)this.GetValue(PathListBoxItemTransformer.OpacityRangeProperty); }
			set { this.SetValue(PathListBoxItemTransformer.OpacityRangeProperty, value); }
		}

		public static readonly DependencyProperty OpacityRangeProperty =
			DependencyProperty.Register("OpacityRange", typeof(Point), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(new Point(0.2, 1.0), new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion

		#region DependencyProperty ScaleRange
		/// <summary>
		/// Gets or sets the range of values to use for the scale.
		/// </summary>
		public Point ScaleRange
		{
			get { return (Point)this.GetValue(PathListBoxItemTransformer.ScaleRangeProperty); }
			set { this.SetValue(PathListBoxItemTransformer.ScaleRangeProperty, value); }
		}

		public static readonly DependencyProperty ScaleRangeProperty =
			DependencyProperty.Register("ScaleRange", typeof(Point), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(new Point(0.2, 1.0), new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion
		
		#region DependencyProperty Shift
		/// <summary>
		/// Gets or sets the amount to shift the offset from the beginning of the path.
		/// </summary>
		public double Shift
		{
			get { return (double)this.GetValue(PathListBoxItemTransformer.ShiftProperty); }
			set { this.SetValue(PathListBoxItemTransformer.ShiftProperty, value); }
		}

		public static readonly DependencyProperty ShiftProperty =
			DependencyProperty.Register("Shift", typeof(double), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(0.0, new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion

		#region DependencyProperty UseGlobalOffset
		/// <summary>
		/// Gets or sets whether to use adjust the offset across all paths by using the GlobalOffset instead of LocalOffset as the starting offset for the item.
		/// </summary>
		public bool UseGlobalOffset
		{
			get { return (bool)this.GetValue(PathListBoxItemTransformer.UseGlobalOffsetProperty); }
			set { this.SetValue(PathListBoxItemTransformer.UseGlobalOffsetProperty, value); }
		}

		public static readonly DependencyProperty UseGlobalOffsetProperty =
			DependencyProperty.Register("UseGlobalOffset", typeof(bool), typeof(PathListBoxItemTransformer),
				new PropertyMetadata(false, new PropertyChangedCallback(PathListBoxItemTransformer.TransformerChanged)));
		#endregion

		/// <summary>
		/// Initializes a new instance of the PathListBoxItemTransformer class.
		/// </summary>
		public PathListBoxItemTransformer()
		{
			this.transformGroup = new TransformGroup();
			this.transformGroup.Children.Add(new ScaleTransform());
			this.transformGroup.Children.Add(new RotateTransform());
			
			this.RenderTransformOrigin = new Point(0.5, 0.5);
			this.RenderTransform = this.transformGroup;
			
			this.Unloaded += this.PathListBoxItemTransformer_Unloaded;
		}

		protected override Size ArrangeOverride(Size arrangeBounds)
		{
			this.UpdateTransform();

			return base.ArrangeOverride(arrangeBounds);
		}

		#region Private Methods
		private void PathListBoxItemTransformer_Unloaded(object sender, RoutedEventArgs e)
		{
			if (this.pathLayoutItem != null)
			{
				this.pathLayoutItem.PathLayoutUpdated -= this.IPathLayoutItem_PathLayoutUpdated;
			}
		}

		private static void TransformerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			PathListBoxItemTransformer transformer = (PathListBoxItemTransformer)d;
			if (transformer != null && transformer.pathLayoutItem != null && transformer.pathListBox != null)
			{
				transformer.UpdateTransform();
			}
		}

		private void IPathLayoutItem_PathLayoutUpdated(object sender, PathLayoutUpdatedEventArgs e)
		{
			this.UpdateTransform();
		}

		private void UpdateTransform()
		{
			if (this.pathListBox == null)
			{
				this.pathListBox = PathListBoxItemTransformer.FindAncestor<PathListBox>(this);
				if (this.pathListBox == null)
				{
					return;
				}
			}

			if (this.pathLayoutItem == null)
			{
				this.pathLayoutItem = PathListBoxItemTransformer.FindAncestor<IPathLayoutItem>(this);
				if (this.pathLayoutItem == null)
				{
					return;
				}
				this.pathLayoutItem.PathLayoutUpdated += this.IPathLayoutItem_PathLayoutUpdated;
			}

			double offset = 0;
			if (this.UseGlobalOffset)
			{
				offset = this.pathLayoutItem.GlobalOffset;
			}
			else
			{
				offset = this.pathLayoutItem.LocalOffset;
			}

			offset = (offset + Math.Abs(this.Shift)) % 1;

			int zIndex = 0;
			int layoutPathIndex = -1;

			zIndex = this.pathLayoutItem.LocalIndex;
			layoutPathIndex = this.pathLayoutItem.LayoutPathIndex;

			// Adjust the offset so that it is normalized over a range of -1 to 1 if falloff from center.
			if (this.IsCentered)
			{
				offset = Math.Abs(PathListBoxItemTransformer.Lerp(-1, 1, (offset + 0.5) % 1.0));
				int numItems = this.pathListBox.GetItemsArrangedCount(layoutPathIndex) / 2;
				zIndex = (int) Math.Round(offset * numItems);
			}
			
			UIElement element = this.pathLayoutItem as UIElement;
			if (element != null && Canvas.GetZIndex(element) != zIndex)
			{
				Canvas.SetZIndex(element, (int)zIndex);
			}

			// Adjust the offset by the user specified easing function.
			if (this.Ease != null)
			{
				offset = this.Ease.Ease(offset);
			}

			// Check value before setting it to avoid layout cycle exceptions.
			if (this.AdjustedOffset != offset)
			{
				this.AdjustedOffset = offset;
			}

			// Use the adjusted offset to interpolate between the user specified start and end for scale and opacity.
			double scale = 1;
			scale = PathListBoxItemTransformer.Lerp(this.ScaleRange.X, this.ScaleRange.Y, this.AdjustedOffset);
			ScaleTransform scaleTransform = (ScaleTransform) this.transformGroup.Children[0];
			scaleTransform.ScaleX = scale;
			scaleTransform.ScaleY = scale;
			
			double angle = PathListBoxItemTransformer.Lerp(this.AngleRange.X, this.AngleRange.Y, this.AdjustedOffset) + this.pathLayoutItem.OrientationAngle;
			RotateTransform rotateTransform = (RotateTransform)this.transformGroup.Children[1];
			rotateTransform.Angle = angle;

			this.Opacity = PathListBoxItemTransformer.Lerp(this.OpacityRange.X, this.OpacityRange.Y, this.AdjustedOffset);
		}

		private static double Lerp(double x, double y, double alpha)
		{
			return x * (1 - alpha) + y * alpha;
		}

		private static T FindAncestor<T>(DependencyObject dependencyObject) where T : class
		{
			if (dependencyObject == null)
			{
				return default(T);
			}

			DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject);
			while (parent != null)
			{
				T testElement = parent as T;
				if (testElement != null)
				{
					return testElement;
				}
				else
				{
					parent = VisualTreeHelper.GetParent(parent);
				}
			}

			return default(T);
		}
		#endregion
	}
}
